/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package maps;

import java.awt.Point;
import java.util.ArrayList;
import java.util.Stack;
import main.Utils;

/**
 *
 * @author menderleit
 */
public abstract class DungeonGenerator {

    public static final int NORTH = 0;
    public static final int SOUTH = 1;
    public static final int WEST = 2;
    public static final int EAST = 3;
    private static DungeonMap map;
    private static int min;
    private static int max;
    private static int lastRoom;
    private static int cX;
    private static int cY;
    private static int layout;
    private static int corridorBend;
    private static DungeonRoom roomPlaced;
    private static boolean secondRoom;

    public static void generate(DungeonMap map, DungeonOptions opts) {
        DungeonGenerator.map = map;
        min = opts.getRoomMin();
        max = opts.getRoomMax();
        layout = opts.getLayout();
        corridorBend = opts.getCorridorBend();
        roomPlaced = null;
        secondRoom = false;
        lastRoom = 0;
        cX = 0;
        cY = 0;
        map.clear();
        placeRooms();
        openRooms();
        makeCorridors();
        cleanUp();
    }

    // Place the rooms.
    private static void placeRooms() {
        secondRoom = false;
        int dungeonArea = map.getWidth() * map.getHeight();
        int roomArea = max * max;
        int maxRooms = dungeonArea / roomArea;
        if (layout == DungeonOptions.LAYOUT_SPARSE) {
            maxRooms = maxRooms / 2;
            layout = DungeonOptions.LAYOUT_SCATTERED;
        }
        if (maxRooms < 1) {
            maxRooms = 1;
        }
        int blockage = Utils.rnd().nextInt(100);
        if (blockage < 50) {
            int bW;
            int bH;
            int angled = Utils.rnd().nextInt(2);
            int bLayout = Utils.rnd().nextInt(2);
            if (bLayout == 0) {
                if (angled == 0) {
                    bW = map.getWidth() / 2 - (Utils.rnd().nextInt((map.getWidth() + 1) / 4));
                    bH = map.getHeight() / 2 - (Utils.rnd().nextInt((map.getHeight() + 1) / 4));
                } else {
                    bW = map.getWidth() / 3 - (Utils.rnd().nextInt((map.getWidth() + 1) / 6));
                    bH = map.getHeight() / 3 - (Utils.rnd().nextInt((map.getHeight() + 1) / 6));
                }
                for (int y = 0; y < bH; y++) {
                    for (int x = 0; x < bW; x++) {
                        map.setBlocked(x, y, true);
                        map.setBlocked(map.getWidth() - x, y, true);
                        map.setBlocked(x, map.getHeight() - y, true);
                        map.setBlocked(map.getWidth() - x, map.getHeight() - y, true);
                    }
                    bW--;
                }
            } else {
                bW = map.getWidth() / 3 - (Utils.rnd().nextInt((map.getWidth() + 1) / 6));
                bH = map.getHeight() / 3 - (Utils.rnd().nextInt((map.getHeight() + 1) / 6));
                for (int y = 0; y < bH; y++) {
                    for (int x = 0; x < bW; x++) {
                        map.setBlocked(map.getWidth() / 2 + x - bW / 2, y, true);
                        map.setBlocked(x, map.getHeight() / 2 + y - bH / 2, true);
                        map.setBlocked(map.getWidth() / 2 + x - bW / 2, map.getHeight() - y, true);
                        map.setBlocked(map.getWidth() - x, map.getHeight() / 2 + y - bH / 2, true);
                    }
                    //bW--;
                }
            }
//            for (int y = 0; y < bH; y++) {
//                for (int x = 0; x < bW; x++) {
//                    map.setBlocked((map.getWidth() / 2 - bW / 2) + x, (map.getHeight() / 2 - bH / 2) + y, true);
//                }
//            }
        }
        if (layout == DungeonOptions.LAYOUT_SCATTERED) {
            for (int i = 0; i < maxRooms; i++) {
                boolean placed = false;
                int count = 0;
                while (!placed && count < 999) {
                    // Generate odd Width and Height.
                    int w = Utils.rnd().nextInt(max - (min - 1)) + min;
                    int h = Utils.rnd().nextInt(max - (min - 1)) + min;
                    while (w % 2 == 0) {
                        w = Utils.rnd().nextInt(max - (min - 1)) + min;
                    }
                    while (h % 2 == 0) {
                        h = Utils.rnd().nextInt(max - (min - 1)) + min;
                    }
                    // Generate odd Position.
                    int xPos = 0;
                    int yPos = 0;
                    while (xPos % 2 == 0) {
                        xPos = Utils.rnd().nextInt(map.getWidth());
                    }
                    while (yPos % 2 == 0) {
                        yPos = Utils.rnd().nextInt(map.getHeight());
                    }
                    placed = placeRoom(xPos, yPos, w, h);
                    count++;
                }
            }
        }
        if (layout == DungeonOptions.LAYOUT_DENSE) {
            for (int y = 1; y < map.getHeight() - 1; y += 2) {
                for (int x = 1; x < map.getWidth() - 1; x += 2) {
                    // Generate odd Width and Height.
                    int w = Utils.rnd().nextInt(max - (min - 1)) + min;
                    int h = Utils.rnd().nextInt(max - (min - 1)) + min;
                    while (w % 2 == 0) {
                        w = Utils.rnd().nextInt(max - (min - 1)) + min;
                    }
                    while (h % 2 == 0) {
                        h = Utils.rnd().nextInt(max - (min - 1)) + min;
                    }
                    int r = Utils.rnd().nextInt(2);
                    if (r == 0) {
                        r = Utils.rnd().nextInt(3);
                        int yo = r * 2;
                        placeRoom(x, y + yo, w, h);
                    }
                }
            }
        }
        if (layout == DungeonOptions.LAYOUT_SYMMETRIC) {
            int xPos = 0;
            int yPos = 0;
            int w = 0;
            int h = 0;
            for (int i = 0; i < maxRooms; i++) {
                boolean placed = false;
                int count = 0;
                secondRoom = false;
                while (!placed && count < 999) {
                    // Generate odd Width and Height.
                    w = Utils.rnd().nextInt(max - (min - 1)) + min;
                    h = Utils.rnd().nextInt(max - (min - 1)) + min;
                    while (w % 2 == 0) {
                        w = Utils.rnd().nextInt(max - (min - 1)) + min;
                    }
                    while (h % 2 == 0) {
                        h = Utils.rnd().nextInt(max - (min - 1)) + min;
                    }
                    // Generate odd Position.
                    xPos = 0;
                    yPos = 0;
                    while (xPos % 2 == 0) {
                        xPos = Utils.rnd().nextInt(map.getWidth() / 2);
                    }
                    while (yPos % 2 == 0) {
                        yPos = Utils.rnd().nextInt(map.getHeight());
                    }
                    placed = placeRoom(xPos, yPos, w, h);
                    count++;
                }
                if (placed) {
                    secondRoom = true;
                    if (xPos < map.getWidth() / 2) {
                        xPos = map.getWidth() - xPos - w;
                    } else {
                        xPos = map.getWidth() / 2 - xPos;
                    }
                    placeRoom(xPos, yPos, w, h);
                }
            }
        }
    }

    // Try to place a new Room.
    private static boolean placeRoom(int xPos, int yPos, int w, int h) {
        boolean placed = false;

        if (layout != DungeonOptions.LAYOUT_SYMMETRIC && !secondRoom) {
            roomPlaced = null;
        }

        // Generate the room if not blocked.
        if (!isRoomBlocked(xPos, yPos, w, h)) {
            placed = true;
            roomPlaced = map.addRoom(xPos, yPos, w, h);

            // Fill in the room.
            for (int y = 0; y < h; y++) {
                for (int x = 0; x < w; x++) {
                    map.setRoom(x + xPos, y + yPos, map.getNumRooms());
                }
            }
            lastRoom = map.getNumRooms();

            // Fill in the perimiter.
            for (int x = -1; x < w + 1; x++) {
                map.setPerimiter(x + xPos, yPos - 1, true);
                map.setPerimiter(x + xPos, yPos + h, true);
            }
            for (int y = 0; y < h; y++) {
                map.setPerimiter(xPos - 1, y + yPos, true);
                map.setPerimiter(xPos + w, y + yPos, true);
            }
        } else {
            if (layout == DungeonOptions.LAYOUT_SYMMETRIC && secondRoom) {
                placed = true;
                DungeonRoom r1 = roomPlaced;
                DungeonRoom r2 = new DungeonRoom(xPos, yPos, w, h);

                xPos = Math.min(r1.getXPosition(), r2.getXPosition());
                w = Math.max(r1.getXPosition() + r1.getWidth(), r2.getXPosition() + r2.getWidth()) - xPos;
                roomPlaced = map.addRoom(xPos, yPos, w, h);
                map.removeRoom(r1);

                // Fill in the room.
                for (int y = 0; y < h; y++) {
                    for (int x = 0; x < w; x++) {
                        map.setPerimiter(x + xPos, y + yPos, false);
                        map.setRoom(x + xPos, y + yPos, map.getNumRooms());
                    }
                }
                lastRoom = map.getNumRooms();

                // Fill in the perimiter.
                for (int x = -1; x < w + 1; x++) {
                    map.setPerimiter(x + xPos, yPos - 1, true);
                    map.setPerimiter(x + xPos, yPos + h, true);
                }
                for (int y = 0; y < h; y++) {
                    map.setPerimiter(xPos - 1, y + yPos, true);
                    map.setPerimiter(xPos + w, y + yPos, true);
                }
            }
        }

        return placed;
    }

    // Check if a room is blocked by another room.
    private static boolean isRoomBlocked(int xPos, int yPos, int width, int height) {
        boolean blocked = false;

        for (int y = yPos; y < yPos + height; y++) {
            for (int x = xPos; x < xPos + width; x++) {
                if (x >= 1 && x < map.getWidth() - 1 && y >= 1 && y < map.getHeight() - 1) {
                    if (map.isRoom(x, y) || /*
                             * map.isPerimiter(x, y) ||
                             */ map.isBlocked(x, y)) {
                        blocked = true;
                    }
                } else {
                    blocked = true;
                }
            }
        }
        return blocked;
    }

    // Add entrances to the rooms.
    private static void openRooms() {
        ArrayList<DungeonRoom> rooms = map.getRooms();
        for (int i = 0; i < rooms.size(); i++) {
            openRoom(rooms.get(i));
        }
    }

    // Add entrances to a room.
    private static void openRoom(DungeonRoom r) {
        int opens = numOpens(r);
        ArrayList<Point> sills = getSills(r);

        for (int i = 0; i < opens; i++) {
            int s = 0;
            if (sills.size() > 1) {
                s = Utils.rnd().nextInt(sills.size() - 1);
            }
            if (sills.size() > 0) {
                Point p = sills.remove(s);
                map.setEntrance(p.x, p.y, true);
                map.setPerimiter(p.x, p.y, false);
                r.addEntrance(p.x, p.y);
            }
        }
    }

    // Calculate the number of entrances for a room.
    private static int numOpens(DungeonRoom r) {
        int rW = r.getWidth() / 2 + 1;
        int rH = r.getHeight() / 2 + 1;
        int flumph = (int) (Math.sqrt(rW * rH));
        if (flumph < 1) {
            flumph = 1;
        }
        int opens = flumph + Utils.rnd().nextInt(flumph);
        return opens;
    }

    // Create a list of possible entrance points.
    private static ArrayList<Point> getSills(DungeonRoom r) {
        ArrayList<Point> sills = new ArrayList();
        for (int x = 0; x < r.getWidth() - 1; x += 2) {
            if (r.getYPosition() >= 3) {
                sills.add(new Point(x + r.getXPosition(), r.getYPosition() - 1));
            }
            if (r.getYPosition() + r.getHeight() < (map.getHeight() - 1)) {
                sills.add(new Point(x + r.getXPosition(), r.getYPosition() + r.getHeight()));
            }
        }
        for (int y = 0; y < r.getHeight(); y += 2) {
            if (r.getXPosition() >= 3) {
                sills.add(new Point(r.getXPosition() - 1, y + r.getYPosition()));
            }
            if (r.getXPosition() + r.getWidth() < (map.getWidth() - 1)) {
                sills.add(new Point(r.getXPosition() + r.getWidth(), y + r.getYPosition()));
            }
        }
        return sills;
    }

    // Generate corridors.
    private static void makeCorridors() {
        //int dir;
        //ArrayList<Integer> dirs;

        for (int offY = 1; offY < map.getHeight(); offY += 2) {
            for (int offX = 1; offX < map.getWidth(); offX += 2) {
                if (!map.isCorridor(offX, offY) && !map.isBlocked(offX, offY)) {
                    cX = offX;
                    cY = offY;
                    int tX = cX;
                    int tY = cY;
                    for (int i = 0; i < 4; i++) {
                        generateCorridor();
                    }
                    cX = tX;
                    cY = tY;
                }
            }
        }
    }

    // Generate a corridor.
    private static void generateCorridor() {
        int dir;
        int oldDir = -1;
        Stack<Point> track = new Stack();
        ArrayList<Integer> dirs;
        boolean done = false;
        while (!done) {
            dirs = new ArrayList();
            for (int i = 0; i < 4; i++) {
                if (canGo(i)) {
                    dirs.add(new Integer(i));
                }
            }
            if (dirs.size() > 0) {
                track.push(new Point(cX, cY));
                dir = dirs.get(Utils.rnd().nextInt(dirs.size()));
                int r = Utils.rnd().nextInt(100);
                if (r < corridorBend) {
                    if (oldDir != -1) {
                        for (int i = 0; i < dirs.size(); i++) {
                            if (oldDir == dirs.get(i)) {
                                dir = oldDir;
                            }
                        }
                    }
                }
                fillSection(dir);
                oldDir = dir;
            } else {
                if (track.size() > 0) {
                    Point p = track.pop();
                    cX = p.x;
                    cY = p.y;
                } else {
                    done = true;
                }
            }
        }
    }

    // Fill in a corridor section.
    private static void fillSection(int dir) {
        switch (dir) {
            case NORTH:
                for (int y = 0; y < 3; y++) {
                    if (!map.isEntrance(cX, cY + y - 2)) {
                        map.setCorridor(cX, cY + y - 2, true);
                    }
                }
                cY -= 2;
                break;
            case SOUTH:
                for (int y = 0; y < 3; y++) {
                    if (!map.isEntrance(cX, cY + y)) {
                        map.setCorridor(cX, cY + y, true);
                    }
                }
                cY += 2;
                break;
            case WEST:
                for (int x = 0; x < 3; x++) {
                    if (!map.isEntrance(cX + x - 2, cY)) {
                        map.setCorridor(cX + x - 2, cY, true);
                    }
                }
                cX -= 2;
                break;
            case EAST:
                for (int x = 0; x < 3; x++) {
                    if (!map.isEntrance(cX + x, cY)) {
                        map.setCorridor(cX + x, cY, true);
                    }
                }
                cX += 2;
                break;
        }
    }

    // Check if the corridor can move in this direction.
    private static boolean canGo(int dir) {
        boolean canMove = true;

        switch (dir) {
            case NORTH:
                if (cY - 2 >= 0) {
                    if (map.isPerimiter(cX, cY - 1)) {
                        canMove = false;
                    }
                    if (map.isCorridor(cX, cY - 2) || map.isBlocked(cX, cY - 2)) {
                        canMove = false;
                    }
                } else {
                    canMove = false;
                }
                break;
            case SOUTH:
                if (cY + 2 < map.getHeight()) {
                    if (map.isPerimiter(cX, cY + 1)) {
                        canMove = false;
                    }
                    if (map.isCorridor(cX, cY + 2) || map.isBlocked(cX, cY + 2)) {
                        canMove = false;
                    }
                } else {
                    canMove = false;
                }
                break;
            case WEST:
                if (cX - 2 >= 0) {
                    if (map.isPerimiter(cX - 1, cY)) {
                        canMove = false;
                    }
                    if (map.isCorridor(cX - 2, cY) || map.isBlocked(cX - 2, cY)) {
                        canMove = false;
                    }
                } else {
                    canMove = false;
                }
                break;
            case EAST:
                if (cX + 2 < map.getWidth()) {
                    if (map.isPerimiter(cX + 1, cY)) {
                        canMove = false;
                    }
                    if (map.isCorridor(cX + 2, cY) || map.isBlocked(cX + 2, cY)) {
                        canMove = false;
                    }
                } else {
                    canMove = false;
                }
                break;
        }
        return canMove;
    }

    // Remove dead ends and Unconnected rooms.
    private static void cleanUp() {
        removeDeadEnds();
        removeUnconnectedRooms();
    }

    // Remove dead ends.
    private static void removeDeadEnds() {
        boolean foundDeadEnd = true;
        while (foundDeadEnd) {
            foundDeadEnd = false;
            for (int y = 0; y < map.getHeight(); y++) {
                for (int x = 0; x < map.getWidth(); x++) {
                    if (map.isCorridor(x, y) || map.isEntrance(x, y)) {
                        int count = getDeadCount(x, y);
                        if (count >= 3) {
                            if (map.isCorridor(x, y)) {
                                map.clear(x, y);
                            }
                            if (map.isEntrance(x, y)) {
                                map.setEntrance(x, y, false);
                                map.setPerimiter(x, y, true);
                                map.removeEntrance(x, y);
                            }
                            foundDeadEnd = true;
                        }
                    }
                }
            }
        }
    }

    // Count the number of empty cells around this point.
    private static int getDeadCount(int x, int y) {
        int count = 0;

        for (int i = 0; i < 4; i++) {
            if (checkDir(x, y, i)) {
                count++;
            }
        }

        return count;
    }

    // See if the cell in this direction is empty.
    private static boolean checkDir(int x, int y, int dir) {
        boolean b = false;

        switch (dir) {
            case NORTH:
                if (y - 1 > 0) {
                    if (!map.isCorridor(x, y - 1) && !map.isRoom(x, y - 1) && !map.isEntrance(x, y - 1)) {
                        b = true;
                    }
                } else {
                    b = true;
                }
                break;
            case SOUTH:
                if (y + 1 < map.getHeight()) {
                    if (!map.isCorridor(x, y + 1) && !map.isRoom(x, y + 1) && !map.isEntrance(x, y + 1)) {
                        b = true;
                    }
                } else {
                    b = true;
                }
                break;
            case WEST:
                if (x - 1 > 0) {
                    if (!map.isCorridor(x - 1, y) && !map.isRoom(x - 1, y) && !map.isEntrance(x - 1, y)) {
                        b = true;
                    }
                } else {
                    b = true;
                }
                break;
            case EAST:
                if (x + 1 > 0) {
                    if (!map.isCorridor(x + 1, y) && !map.isRoom(x + 1, y) && !map.isEntrance(x + 1, y)) {
                        b = true;
                    }
                } else {
                    b = true;
                }
                break;
        }
        return b;
    }

    // Remove unconnected rooms.
    private static void removeUnconnectedRooms() {
        ArrayList<DungeonRoom> rooms = map.getRooms();
        for (int i = 0; i < rooms.size(); i++) {
            if (rooms.get(i).getNumEntrances() == 0) {
                DungeonRoom r = rooms.get(i);
                for (int y = 0; y < r.getWidth(); y++) {
                    for (int x = 0; x < r.getWidth(); x++) {
                        map.setRoom(x + r.getXPosition(), y + r.getYPosition(), false);
                    }
                }
            }
        }
    }
}
